<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="generator" content="VNote">

    <title>14_基于方法的客户端-服务端服务</title>
    <link rel="icon" href="https://github.com/tamlok/vnote/raw/master/src/resources/icons/vnote.ico">

    <style type="text/css">
    /* STYLE_GLOBAL_PLACE_HOLDER */
    </style>

    <style type="text/css">
    *,
*::before,
*::after {
  box-sizing: border-box;
}

.container-fluid {
    width: 100%;
    padding-right: 15px;
    padding-left: 15px;
    margin-right: auto;
    margin-left: auto;
}

.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto {
    position: relative;
    width: 100%;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
}

.col-12 {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 100%;
    flex: 0 0 100%;
    max-width: 100%;
}

@media (min-width: 768px) {
    .col-md-3 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 25%;
        flex: 0 0 25%;
        max-width: 25%;
    }
}

@media (min-width: 768px) {
    .col-md-9 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 75%;
        flex: 0 0 75%;
        max-width: 75%;
    }
}

@media (min-width: 1200px) {
    .col-xl-2 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 16.666667%;
        flex: 0 0 16.666667%;
        max-width: 16.666667%;
    }
}

@media (min-width: 1200px) {
    .col-xl-10 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 83.333333%;
        flex: 0 0 83.333333%;
        max-width: 83.333333%;
    }
}

@media (min-width: 768px) {
    .pt-md-3, .py-md-3 {
        padding-top: 1rem!important;
    }
}

@media (min-width: 768px) {
    .pb-md-3, .py-md-3 {
        padding-bottom: 1rem!important;
    }
}

@media (min-width: 768px) {
    .pl-md-5, .px-md-5 {
        padding-left: 3rem!important;
    }
}

.d-none {
    display: none!important;
}

@media (min-width: 1200px) {
    .d-xl-block {
        display: block!important;
    }
}

@media (min-width: 768px) {
    .d-md-block {
        display: block!important;
    }
}

.bd-content {
    -webkit-box-ordinal-group: 1;
    -ms-flex-order: 0;
    order: 0;
}

.bd-toc {
    position: -webkit-sticky;
    position: sticky;
    top: 4rem;
    height: calc(100vh - 10rem);
    overflow-y: auto;
}

.bd-toc {
    -webkit-box-ordinal-group: 2;
    -ms-flex-order: 1;
    order: 1;
    padding-top: 1.5rem;
    padding-bottom: 1.5rem;
    font-size: .875rem;
}

.section-nav {
    padding-left: 0;
}

.section-nav ul {
    font-size: .875rem;
    list-style-type: none;
}

.section-nav li {
    font-size: .875rem;
}

.section-nav a {
    color: inherit !important;
}

.row {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-wrap: wrap;
    flex-wrap: wrap;
    margin-right: -15px;
    margin-left: -15px;
}

@media (min-width: 1200px) {
    .flex-xl-nowrap {
        flex-wrap: nowrap !important;
    }
}

#floating-button {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    background: #00897B;
    position: fixed;
    top: .5rem;
    right: .5rem;
    cursor: pointer;
    box-shadow: 0px 2px 5px #666;
}

#floating-button .more {
    color: #F5F5F5;
    position: absolute;
    top: 0;
    display: block;
    bottom: 0;
    left: 0;
    right: 0;
    text-align: center;
    padding: 0;
    margin: 0;
    line-height: 2.5rem;
    font-size: 2rem;
    font-family: 'monospace';
    font-weight: 300;
}

.hide-none {
    display: none !important;
}

.col-expand {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 100% !important;
    flex: 0 0 100% !important;
    max-width: 100% !important;
    padding-right: 3rem !important;
}

.outline-bold {
    font-weight: bolder !important;
}

@media print {
    #floating-button {
        display: none !important;
    }
}

    @keyframes flash { 
  0% { color: rgb(128, 203, 196); }
  10% { color: rgb(0, 137, 123); }
  40% { color: rgb(0, 137, 123); }
  50% { color: rgb(128, 203, 196); }
  60% { color: rgb(0, 137, 123); }
  90% { color: rgb(0, 137, 123); }
}
.highlighted-anchor { animation: flash 1s; }
div.mark-rect { background: transparent; border: 5px solid rgb(87, 104, 196); border-radius: 2px; position: absolute; }
#vnote-footer { width: 100%; text-align: center; opacity: 0.2; margin-top: 3rem; }
#vnote-footer p { font-size: 0.8rem; }
#vnote-footer a { color: inherit !important; }
x-eqs { display: flex; flex-direction: row; align-content: space-between; align-items: center; }
x-eqs > x-eqn { width: 100%; margin-left: 3rem; }
x-eqs > span { text-align: right; }
.view-image, .view-svg { transition: 0.3s; }
.modal-box { display: none; position: fixed; z-index: 1000; padding-top: 50px; left: 0px; top: 0px; width: 100%; height: 100%; overflow: hidden; background-color: rgba(68, 68, 68, 0.952941); }
.modal-content { margin: auto; display: block; width: auto; height: auto; cursor: move; }
.modal-content { animation-name: zoom; animation-duration: 0.6s; }
@-webkit-keyframes zoom { 
  0% { transform: scale(0); }
  100% { transform: scale(1); }
}
@keyframes zoom { 
  0% { transform: scale(0); }
  100% { transform: scale(1); }
}
span.modal-close { position: absolute; z-index: 1000; top: 15px; right: 35px; color: rgb(218, 218, 218); font-size: 40px; font-weight: bold; transition: 0.3s; }
span.modal-close:hover, span.modal-close:focus { color: rgb(238, 238, 238); text-decoration: none; cursor: pointer; }
@media print {
  pre, pre code, td.hljs-ln-code { white-space: pre-wrap !important; word-break: break-all !important; }
  code, a { word-break: break-all !important; }
  div.flowchart-diagram, div.mermaid-diagram, div.plantuml-diagram { overflow: hidden !important; }
  img { max-width: 100% !important; height: auto !important; }
  #vnote-footer { display: none !important; }
}
.alert { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 0.25rem; }
.alert-primary { color: rgb(0, 64, 133); background-color: rgb(204, 229, 255); border-color: rgb(184, 218, 255); }
.alert-secondary { color: rgb(56, 61, 65); background-color: rgb(226, 227, 229); border-color: rgb(214, 216, 219); }
.alert-success { color: rgb(21, 87, 36); background-color: rgb(212, 237, 218); border-color: rgb(195, 230, 203); }
.alert-info { color: rgb(12, 84, 96); background-color: rgb(209, 236, 241); border-color: rgb(190, 229, 235); }
.alert-warning { color: rgb(133, 100, 4); background-color: rgb(255, 243, 205); border-color: rgb(255, 238, 186); }
.alert-danger { color: rgb(114, 28, 36); background-color: rgb(248, 215, 218); border-color: rgb(245, 198, 203); }
.alert-light { color: rgb(129, 129, 130); background-color: rgb(254, 254, 254); border-color: rgb(253, 253, 254); }
.alert-dark { color: rgb(27, 30, 33); background-color: rgb(214, 216, 217); border-color: rgb(198, 200, 202); }
.vnote-anchor { font-weight: 400; color: rgba(0, 123, 255, 0.498039); transition: color 0.16s linear; padding-left: 0.375em; -webkit-font-smoothing: antialiased; text-decoration: none; opacity: 0; }
.vnote-anchor:hover { color: rgb(0, 123, 255); text-decoration: none; opacity: 1; }
.vnote-anchor::after { content: attr(data-anchor-icon); }
.vnote-btn { position: relative; display: inline-block; padding: 6px 12px; font-size: 13px; font-weight: 700; line-height: 20px; white-space: nowrap; vertical-align: middle; cursor: pointer; border: none; user-select: none; -webkit-appearance: none; }
.vnote-copy-clipboard-btn { transition: opacity 0.3s ease-in-out; opacity: 0; padding: 2px 6px; position: absolute; top: 5px; right: 5px; }
pre:hover .vnote-copy-clipboard-btn { opacity: 1; }
pre.vnote-snippet { position: relative; }
body { margin: 0px auto; font-family: "Segoe UI", Helvetica, sans-serif, Tahoma, Arial, Geneva, Georgia, Palatino, "Times New Roman", "Hiragino Sans GB", 冬青黑体, "Microsoft YaHei", 微软雅黑, "Microsoft YaHei UI", "WenQuanYi Micro Hei", 文泉驿雅黑, Dengxian, 等线体, STXihei, 华文细黑, "Liberation Sans", "Droid Sans", NSimSun, 新宋体, SimSun, 宋体; color: rgb(34, 34, 34); line-height: 1.5; padding: 15px; background: rgb(238, 238, 238); font-size: 16px; }
h1, h2, h3, h4, h5, h6 { color: rgb(34, 34, 34); font-weight: bold; margin-top: 20px; margin-bottom: 10px; padding: 0px; }
p { padding: 0px; margin-top: 16px; margin-bottom: 16px; }
h1 { font-size: 26px; }
h2 { font-size: 24px; }
h3 { font-size: 22px; }
h4 { font-size: 20px; }
h5 { font-size: 19px; }
h6 { font-size: 18px; }
a { color: rgb(0, 153, 255); margin: 0px; padding: 0px; vertical-align: baseline; text-decoration: none; word-break: break-word; }
a:hover { text-decoration: underline; color: rgb(255, 102, 0); }
a:visited { color: purple; }
ul, ol { padding: 0px 0px 0px 24px; }
li { line-height: 24px; }
li ul, li ol { margin-left: 16px; }
p, ul, ol { font-size: 16px; line-height: 24px; }
pre { display: block; overflow-y: hidden; overflow-x: auto; tab-size: 4; }
code { font-family: Consolas, Monaco, monospace, Courier; color: rgb(142, 36, 170); word-break: break-word; }
pre code { display: block; overflow-x: auto; padding: 0.5em; color: rgb(34, 34, 34); background-color: rgb(224, 224, 224); border-left: 0.5em solid rgb(0, 137, 123); line-height: 1.5; font-family: Consolas, Monaco, monospace, Courier; white-space: pre; tab-size: 4; }
pre code.markdown-metadata { border-left: 0.5em solid rgb(128, 203, 196); }
aside { display: block; float: right; width: 390px; }
blockquote { color: rgb(102, 102, 102); border-left: 0.5em solid rgb(122, 122, 122); padding: 0px 1em; margin-left: 0px; }
blockquote p { color: rgb(102, 102, 102); }
hr { display: block; text-align: left; margin: 1em 0px; border: none; height: 2px; background: rgb(153, 153, 153); }
table { padding: 0px; margin: 1rem 0.5rem; border-collapse: collapse; }
table tr { border-top: 2px solid rgb(204, 204, 204); background-color: white; margin: 0px; padding: 0px; }
table tr:nth-child(2n) { background-color: rgb(248, 248, 248); }
table tr th { font-weight: bold; border: 2px solid rgb(204, 204, 204); margin: 0px; padding: 6px 13px; }
table tr td { border: 2px solid rgb(204, 204, 204); margin: 0px; padding: 6px 13px; }
table tr th :first-child, table tr td :first-child { margin-top: 0px; }
table tr th :last-child, table tr td :last-child { margin-bottom: 0px; }
div.mermaid-diagram { margin: 16px 0px; overflow-y: hidden; }
div.flowchart-diagram { padding: 0px 5px; margin: 16px 0px; width: fit-content; overflow: hidden; }
div.wavedrom-diagram { padding: 0px 5px; margin: 16px 0px; width: fit-content; overflow: hidden; }
div.plantuml-diagram { padding: 5px 5px 0px; margin: 16px 0px; width: fit-content; overflow: hidden; }
.img-package { text-align: center; }
img.img-center { display: block; margin-left: auto; margin-right: auto; }
span.img-caption { min-width: 20%; max-width: 80%; display: inline-block; padding: 10px; margin: 0px auto; border-bottom: 1px solid rgb(192, 192, 192); color: rgb(108, 108, 108); text-align: center; line-height: 1.5; }
.emoji_zero, .emoji_one, .emoji_two, .emoji_three, .emoji_four, .emoji_five, .emoji_six, .emoji_seven, .emoji_eight, .emoji_nine { margin-left: 5px; margin-right: 8px; }
div.preview-hint { opacity: 0.5; margin-top: 30%; margin-bottom: 30%; align-items: center; display: flex; flex-direction: column; justify-content: center; }
table.hljs-ln tr { border: none; background-color: transparent; }
table.hljs-ln tr td { border: none; background-color: transparent; }
table.hljs-ln tr td.hljs-ln-numbers { user-select: none; text-align: center; color: rgb(170, 170, 170); border-right: 1px solid rgb(204, 204, 204); vertical-align: top; padding-right: 5px; white-space: nowrap; }
table.hljs-ln tr td.hljs-ln-code { padding-left: 10px; }
::-webkit-scrollbar { background-color: rgb(234, 234, 234); width: 14px; height: 14px; border: none; }
::-webkit-scrollbar-corner { background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-button { height: 14px; width: 14px; background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-button:hover { background-color: rgb(208, 208, 208); }
::-webkit-scrollbar-button:active { background-color: rgb(178, 178, 178); }
::-webkit-scrollbar-track { background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-thumb { border: none; background-color: rgb(218, 218, 218); }
::-webkit-scrollbar-thumb:hover { background-color: rgb(208, 208, 208); }
::-webkit-scrollbar-thumb:active { background-color: rgb(178, 178, 178); }
::-webkit-scrollbar-button:horizontal:increment { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(-90 256.00000000000006,256) " id="svg_1">   <polygon fill="%23333333" id="svg_2" points="128,192 256,320 384,192  "/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:horizontal:decrement { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(90 255.99999999999997,256.00000000000006) " id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:vertical:increment { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="null" id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:vertical:decrement { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(180 255.99999999999997,256) " id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::selection { background: rgb(25, 118, 210); color: rgb(238, 238, 238); }
.modal-box { background-color: rgba(234, 234, 234, 0.952941); }
span.modal-close { color: rgb(102, 102, 102); }
span.modal-close:hover, span.modal-close:focus { color: rgb(34, 34, 34); }
.hljs { display: block; overflow-x: auto; padding: 0.5em; background: rgb(224, 224, 224); }
.hljs, .hljs-subst { color: rgb(54, 54, 54); }
.hljs-comment { color: rgb(118, 118, 118); }
.hljs-keyword, .hljs-attribute, .hljs-selector-tag, .hljs-meta-keyword, .hljs-doctag, .hljs-name { color: rgb(0, 0, 238); }
.hljs-type, .hljs-string, .hljs-number, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion { color: rgb(136, 0, 0); }
.hljs-title, .hljs-section { color: rgb(136, 0, 0); font-weight: bold; }
.hljs-regexp, .hljs-symbol, .hljs-variable, .hljs-template-variable, .hljs-link, .hljs-selector-attr, .hljs-selector-pseudo { color: rgb(188, 96, 96); }
.hljs-literal { color: rgb(175, 0, 215); }
.hljs-built_in, .hljs-bullet, .hljs-code, .hljs-addition { color: rgb(0, 135, 0); }
.hljs-meta { color: rgb(31, 113, 153); }
.hljs-meta-string { color: rgb(77, 153, 191); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: bold; }
.mermaid-diagram .mermaid .label { color: rgb(51, 51, 51); }
.mermaid-diagram .node rect, .mermaid-diagram .node circle, .mermaid-diagram .node ellipse, .mermaid-diagram .node polygon { fill: rgb(236, 236, 255); stroke: rgb(204, 204, 255); stroke-width: 1px; }
.mermaid-diagram .edgePath .path { stroke: rgb(51, 51, 51); }
.mermaid-diagram .edgeLabel { background-color: rgb(232, 232, 232); }
.mermaid-diagram .cluster rect { fill: rgb(255, 255, 222) !important; rx: 4 !important; stroke: rgb(170, 170, 51) !important; stroke-width: 1px !important; }
.mermaid-diagram .cluster text { fill: rgb(51, 51, 51); }
.mermaid-diagram .actor { stroke: rgb(204, 204, 255); fill: rgb(236, 236, 255); }
.mermaid-diagram text.actor { fill: black; stroke: none; }
.mermaid-diagram .actor-line { stroke: grey; }
.mermaid-diagram .messageLine0 { stroke-width: 1.5; stroke: rgb(51, 51, 51); }
.mermaid-diagram .messageLine1 { stroke-width: 1.5; stroke: rgb(51, 51, 51); }
.mermaid-diagram #arrowhead { fill: rgb(51, 51, 51); }
.mermaid-diagram #crosshead path { fill: rgb(51, 51, 51) !important; stroke: rgb(51, 51, 51) !important; }
.mermaid-diagram .messageText { fill: rgb(51, 51, 51); stroke: none; }
.mermaid-diagram .labelBox { stroke: rgb(204, 204, 255); fill: rgb(236, 236, 255); }
.mermaid-diagram .labelText { fill: black; stroke: none; }
.mermaid-diagram .loopText { fill: black; stroke: none; }
.mermaid-diagram .loopLine { stroke-width: 2; stroke: rgb(204, 204, 255); }
.mermaid-diagram .note { stroke: rgb(170, 170, 51); fill: rgb(255, 245, 173); }
.mermaid-diagram .noteText { fill: black; stroke: none; font-family: "trebuchet ms", verdana, arial; font-size: 14px; }
.mermaid-diagram .section { stroke: none; opacity: 0.2; }
.mermaid-diagram .section0 { fill: rgba(102, 102, 255, 0.490196); }
.mermaid-diagram .section2 { fill: rgb(255, 244, 0); }
.mermaid-diagram .section1, .mermaid-diagram .section3 { fill: white; opacity: 0.2; }
.mermaid-diagram .sectionTitle0 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle1 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle2 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle3 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle { text-anchor: start; font-size: 11px; }
.mermaid-diagram .grid .tick { stroke: lightgrey; opacity: 0.3; shape-rendering: crispEdges; }
.mermaid-diagram .grid path { stroke-width: 0; }
.mermaid-diagram .today { fill: none; stroke: red; stroke-width: 2px; }
.mermaid-diagram .task { stroke-width: 2; }
.mermaid-diagram .taskText { text-anchor: middle; font-size: 11px; }
.mermaid-diagram .taskTextOutsideRight { fill: black; text-anchor: start; font-size: 11px; }
.mermaid-diagram .taskTextOutsideLeft { fill: black; text-anchor: end; font-size: 11px; }
.mermaid-diagram .taskText0, .mermaid-diagram .taskText1, .mermaid-diagram .taskText2, .mermaid-diagram .taskText3 { fill: white; }
.mermaid-diagram .task0, .mermaid-diagram .task1, .mermaid-diagram .task2, .mermaid-diagram .task3 { fill: rgb(138, 144, 221); stroke: rgb(83, 79, 188); }
.mermaid-diagram .taskTextOutside0, .mermaid-diagram .taskTextOutside2 { fill: black; }
.mermaid-diagram .taskTextOutside1, .mermaid-diagram .taskTextOutside3 { fill: black; }
.mermaid-diagram .active0, .mermaid-diagram .active1, .mermaid-diagram .active2, .mermaid-diagram .active3 { fill: rgb(191, 199, 255); stroke: rgb(83, 79, 188); }
.mermaid-diagram .activeText0, .mermaid-diagram .activeText1, .mermaid-diagram .activeText2, .mermaid-diagram .activeText3 { fill: black !important; }
.mermaid-diagram .done0, .mermaid-diagram .done1, .mermaid-diagram .done2, .mermaid-diagram .done3 { stroke: grey; fill: lightgrey; stroke-width: 2; }
.mermaid-diagram .doneText0, .mermaid-diagram .doneText1, .mermaid-diagram .doneText2, .mermaid-diagram .doneText3 { fill: black !important; }
.mermaid-diagram .crit0, .mermaid-diagram .crit1, .mermaid-diagram .crit2, .mermaid-diagram .crit3 { stroke: rgb(255, 136, 136); fill: red; stroke-width: 2; }
.mermaid-diagram .activeCrit0, .mermaid-diagram .activeCrit1, .mermaid-diagram .activeCrit2, .mermaid-diagram .activeCrit3 { stroke: rgb(255, 136, 136); fill: rgb(191, 199, 255); stroke-width: 2; }
.mermaid-diagram .doneCrit0, .mermaid-diagram .doneCrit1, .mermaid-diagram .doneCrit2, .mermaid-diagram .doneCrit3 { stroke: rgb(255, 136, 136); fill: lightgrey; stroke-width: 2; cursor: pointer; shape-rendering: crispEdges; }
.mermaid-diagram .doneCritText0, .mermaid-diagram .doneCritText1, .mermaid-diagram .doneCritText2, .mermaid-diagram .doneCritText3 { fill: black !important; }
.mermaid-diagram .activeCritText0, .mermaid-diagram .activeCritText1, .mermaid-diagram .activeCritText2, .mermaid-diagram .activeCritText3 { fill: black !important; }
.mermaid-diagram .titleText { text-anchor: middle; font-size: 18px; fill: black; }
.mermaid-diagram .node text { font-family: "trebuchet ms", verdana, arial; font-size: 14px; }
.mermaid-diagram div.mermaidTooltip { position: absolute; text-align: center; max-width: 200px; padding: 2px; font-family: "trebuchet ms", verdana, arial; font-size: 12px; background: rgb(255, 255, 222); border: 1px solid rgb(170, 170, 51); border-radius: 2px; pointer-events: none; z-index: 100; }
#mermaid-diagram-1 .node > rect { }
#mermaid-diagram-1 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-1 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-1 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-1 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }

    </style>

    <script type="text/javascript">
var toc = [];

var setVisible = function(node, visible) {
    var cl = 'hide-none';
    if (visible) {
        node.classList.remove(cl);
    } else {
        node.classList.add(cl);
    }
};

var isVisible = function(node) {
    var cl = 'hide-none';
    return !node.classList.contains(cl);
};

var setPostContentExpanded = function(node, expanded) {
    var cl = 'col-expand';
    if (expanded) {
        node.classList.add(cl);
    } else {
        node.classList.remove(cl);
    }
};

var setOutlinePanelVisible = function(visible) {
    var outlinePanel = document.getElementById('outline-panel');
    var postContent = document.getElementById('post-content');

    setVisible(outlinePanel, visible);
    setPostContentExpanded(postContent, !visible);
};

var isOutlinePanelVisible = function() {
    var outlinePanel = document.getElementById('outline-panel');
    return isVisible(outlinePanel);
};

window.addEventListener('load', function() {
    var outlinePanel = document.getElementById('outline-panel');
    outlinePanel.style.display = 'initial';

    var floatingContainer = document.getElementById('container-floating');
    floatingContainer.style.display = 'initial';

    var outlineContent = document.getElementById('outline-content');
    var postContent = document.getElementById('post-content');

    // Escape @text to Html.
    var escapeHtml = function(text) {
        var map = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#039;'
        };

        return text.replace(/[&<>"']/g, function(m) { return map[m]; });
    }

    // Fetch the outline.
    var headers = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");
    toc = [];
    for (var i = 0; i < headers.length; ++i) {
        var header = headers[i];

        toc.push({
            level: parseInt(header.tagName.substr(1)),
            anchor: header.id,
            title: escapeHtml(header.textContent)
        });
    }

    if (toc.length == 0) {
        setOutlinePanelVisible(false);
        setVisible(floatingContainer, false);
        return;
    }

    var baseLevel = baseLevelOfToc(toc);
    var tocTree = tocToTree(toPerfectToc(toc, baseLevel), baseLevel);

    outlineContent.innerHTML = tocTree;
    setOutlinePanelVisible(true);
    setVisible(floatingContainer, true);
});

// Return the topest level of @toc, starting from 1.
var baseLevelOfToc = function(p_toc) {
    var level = -1;
    for (i in p_toc) {
        if (level == -1) {
            level = p_toc[i].level;
        } else if (level > p_toc[i].level) {
            level = p_toc[i].level;
        }
    }

    if (level == -1) {
        level = 1;
    }

    return level;
};

// Handle wrong title levels, such as '#' followed by '###'
var toPerfectToc = function(p_toc, p_baseLevel) {
    var i;
    var curLevel = p_baseLevel - 1;
    var perfToc = [];
    for (i in p_toc) {
        var item = p_toc[i];

        // Insert empty header.
        while (item.level > curLevel + 1) {
            curLevel += 1;
            var tmp = { level: curLevel,
                        anchor: '',
                        title: '[EMPTY]'
                      };
            perfToc.push(tmp);
        }

        perfToc.push(item);
        curLevel = item.level;
    }

    return perfToc;
};

var itemToHtml = function(item) {
    return '<a href="#' + item.anchor + '" data="' + item.anchor + '">' + item.title + '</a>';
};

// Turn a perfect toc to a tree using <ul>
var tocToTree = function(p_toc, p_baseLevel) {
    var i;
    var front = '<li>';
    var ending = ['</li>'];
    var curLevel = p_baseLevel;
    for (i in p_toc) {
        var item = p_toc[i];
        if (item.level == curLevel) {
            front += '</li>';
            front += '<li>';
            front += itemToHtml(item);
        } else if (item.level > curLevel) {
            // assert(item.level - curLevel == 1)
            front += '<ul>';
            ending.push('</ul>');
            front += '<li>';
            front += itemToHtml(item);
            ending.push('</li>');
            curLevel = item.level;
        } else {
            while (item.level < curLevel) {
                var ele = ending.pop();
                front += ele;
                if (ele == '</ul>') {
                    curLevel--;
                }
            }
            front += '</li>';
            front += '<li>';
            front += itemToHtml(item);
        }
    }
    while (ending.length > 0) {
        front += ending.pop();
    }
    front = front.replace("<li></li>", "");
    front = '<ul>' + front + '</ul>';
    return front;
};

var toggleMore = function() {
    if (toc.length == 0) {
        return;
    }

    var p = document.getElementById('floating-more');
    if (isOutlinePanelVisible()) {
        p.textContent = '<';
        setOutlinePanelVisible(false);
    } else {
        p.textContent = '>';
        setOutlinePanelVisible(true);
    }
};

window.addEventListener('scroll', function() {
    if (toc.length == 0 || !isOutlinePanelVisible()) {
        return;
    }

    var postContent = document.getElementById('post-content');
    var scrollTop = document.documentElement.scrollTop
                    || document.body.scrollTop
                    || window.pageYOffset;
    var eles = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");

    if (eles.length == 0) {
        return;
    }

    var idx = -1;
    var biaScrollTop = scrollTop + 50;
    for (var i = 0; i < eles.length; ++i) {
        if (biaScrollTop >= eles[i].offsetTop) {
            idx = i;
        } else {
            break;
        }
    }

    var header = '';
    if (idx != -1) {
        header = eles[idx].id;
    }

    highlightItemOnlyInOutline(header);
});

var highlightItemOnlyInOutline = function(id) {
    var cl = 'outline-bold';
    var outlineContent = document.getElementById('outline-content');
    var eles = outlineContent.querySelectorAll("a");
    var target = null;
    for (var i = 0; i < eles.length; ++i) {
        var ele = eles[i];
        if (ele.getAttribute('data') == id) {
            target = ele;
            ele.classList.add(cl);
        } else {
            ele.classList.remove(cl);
        }
    }

    // TODO: scroll target into view within the outline panel scroll area.
};

</script>


<!-- HEAD_PLACE_HOLDER -->
</head>
<body>
<div class="container-fluid">
<div class="row flex-xl-nowrap">
    <div id="outline-panel" style="display:none;" class="d-none d-md-block d-xl-block col-md-3 col-xl-2 bd-toc">
        <div id="outline-content" class="section-nav"></div>
    </div>
    <div id="post-content" class="col-12 col-md-9 col-xl-10 py-md-3 pl-md-5 bd-content">
    <div style="page-break-after: always;"></div>
<h1 id="toc_0">14. 基于方法的客户端-服务端服务<a class="vnote-anchor" href="#toc_0" data-anchor-icon="#"></a></h1>
<p><img src="https://github.com/wai818/mORMot-Learning/blob/master/mORMot_Picture/cartoon04.png?raw=true" alt="" class="view-image"></p>
<p>  要在Synopse mORMot框架中实现服务，第一种方法是在服务端定义发布方法，然后使用带JSON或URL参数的简单函数在客户端上按预期对请求进行编码和解码。</p>
<p>  我们将实现与上面官方Embarcadero docwiki页面中相同的示例，计算两个数字之和，非常实用的服务，不是吗？</p>
<h2 id="toc_1">14.1. 在服务器端发布服务<a class="vnote-anchor" href="#toc_1" data-anchor-icon="#"></a></h2>
<p>  在服务端，我们需要在<code>TSQLRestServer</code>（更确切地说是包含SQlite3引擎的<code>TSQLRestServerDB</code>类，或者更轻的<code>TSQLRestServerFullMemory</code>类服务器，这足以满足我们的目的）派生类中添加一个新的<code>published</code>方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TSQLRestServerTest</span> = <span class="hljs-keyword">class</span>(TSQLRestServerFullMemory)
   (...)
  <span class="hljs-keyword">published</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Sum</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  方法名称（“Sum”）将用于URI编码，并从ModelRoot/Sum URL远程调用。 ModelRoot是在应用程序模型中的Root参数定义的。</p>
<p>  与所有服务端方法一样，此方法必须具有与<code>TSQLRestServerCallBack</code>原型相同的精确参数定义，即只有一个Ctxt参数，它引用整个执行上下文：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  TSQLRestServerCallBack = <span class="hljs-function"><span class="hljs-keyword">procedure</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span> <span class="hljs-title">of</span> <span class="hljs-title">object</span>;</span>
</code></pre>
<p>  然后我们实现这个方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRestServerTest</span>.<span class="hljs-title">Sum</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
<span class="hljs-keyword">begin</span>
  Ctxt.Results([Ctxt[<span class="hljs-string">'a'</span>]+Ctxt[<span class="hljs-string">'b'</span>]]);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  Ctxt变量发布一些名为<code>InputInt[] InputDouble[] InputUTF8[]</code>和<code>Input[]</code>的属性，能够直接从名称中检索参数值，分别为Integer/Int64、double、RawUTF8或variant。返回变量值的<code>Ctxt.Input[]</code>数组属性已被定义为<code>TSQLRestServerURIContext</code>类的默认数组属性，因此编写<code>Ctxt['a']</code>与编写<code>Ctxt.Input['a']</code>相同。</p>
<p>  因此，上面使用<code>Ctxt[]</code>或<code>Ctxt.Input[]</code>的代码将通过变体进行转换，这可能有点慢，并且在字符串内容的情况下，可能会丢弃旧的非Unicode版本的Delphi的某些内容。因此，最好使用与您的值类型对应的精确预期的<code>Input*[]</code>属性。在处理文本时它甚至更有意义，即在这种情况下使用<code>InputUTF8[]</code>。对于我们的浮点计算方法，我们可以将其编码为：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRestServerTest</span>.<span class="hljs-title">Sum</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">with</span> Ctxt <span class="hljs-keyword">do</span>
    Results([InputDouble[<span class="hljs-string">'a'</span>]+InputDouble[<span class="hljs-string">'b'</span>]]);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  如果参数在URI中不可用，那么这些方法将引发<code>EParsingException</code>异常。因此，您可能希望使用<code>InputExists[]</code>或<code>InputIntOrVoid[] InputDoubleOrVoid[] InputUTF8OrVoid[] InputOrVoid[]</code>方法，这些方法不会引发任何异常但返回空值（即<code>0</code>，<code>""</code>或<code>Unassigned</code>）。</p>
<p>  <code>Ctxt.Results([])</code>方法用于将服务值作为一个JSON对象返回，其中包含一个<code>"Result"</code>成员，默认MIME类型为<code>JSON_CONTENT_TYPE</code>。</p>
<p>  例如，以下URI请求：</p>
<pre><code class="hljs"> GET /root/Sum?a=3.12&amp;b=4.2
</code></pre>
<p>  我们的服务器方法返回以下JSON对象：</p>
<pre><code class="lang-json hljs"> {<span class="hljs-attr">"Result"</span>:<span class="hljs-number">7.32</span>}
</code></pre>
<p>  也就是说，一个完美的AJAX友好请求。</p>
<p>  请注意，所有参数都应该是不区分大小写的<code>'A'..'Z','0'..'9'</code>'字符。</p>
<p>  重要的一点是，回调方法的实现必须是线程安全的 - 正如多线程应用程序的线程安全和安全锁所述。 事实上，<code>TSQLRestServer.URI</code>方法希望这样的回调能够处理它们的线程安全性。 处理实现中的关键部分可能还需要做一些工作，但实际上，这是实现性能和可伸缩性的最佳方法：资源锁定可以在最小的代码级别进行。</p>
<h2 id="toc_2">14.2. 客户端定义<a class="vnote-anchor" href="#toc_2" data-anchor-icon="#"></a></h2>
<p>  客户端通过调用专用方法，并提供服务名称（<code>'sum'</code>）及其相关参数来实现：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Sum</span><span class="hljs-params">(aClient: TSQLRestClientURI; a, b: double)</span>:</span> double;
<span class="hljs-keyword">var</span> err: integer;
<span class="hljs-keyword">begin</span>
  val(aClient.CallBackGetResult(<span class="hljs-string">'sum'</span>,[<span class="hljs-string">'a'</span>,a,<span class="hljs-string">'b'</span>,b]),Result,err);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  您甚至可以在专用的客户端方法中实现此方法 - 这是有道理的：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TMyClient</span> = <span class="hljs-keyword">class</span>(TSQLHttpClient) <span class="hljs-comment">// could be TSQLRestClientURINamedPipe</span>
  (...)
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Sum</span><span class="hljs-params">(a, b: double)</span>:</span> double;
  (...)

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TMyClient</span>.<span class="hljs-title">Sum</span><span class="hljs-params">(a, b: double)</span>:</span> double;
<span class="hljs-keyword">var</span> err: integer;
<span class="hljs-keyword">begin</span>
  val(CallBackGetResult(<span class="hljs-string">'sum'</span>,[<span class="hljs-string">'a'</span>,a,<span class="hljs-string">'b'</span>,b]),Result,err);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  后面的实现在实际应用中是首选的。</p>
<p>  您必须像往常一样使用相同的数据库模型创建服务器实例和相应的<code>TSQLRestClientURI</code>（或<code>TMyClient</code>）...</p>
<p>  在客户端，您可以使用<code>CallBackGetResult</code>方法使用名称及其预期参数调用服务，或使用<code>UrlEncode()</code>函数创建自己的调用方。 请注意，您可以通过在方法参数中使用一些TObject将大多数类实例指定为其JSON表示：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TMyClient</span>.<span class="hljs-title">SumMyObject</span><span class="hljs-params">(a, b: TMyObject)</span>:</span> double;
<span class="hljs-keyword">var</span> err: integer;
<span class="hljs-keyword">begin</span>
  val(CallBackGetResult(<span class="hljs-string">'summyobject'</span>,[<span class="hljs-string">'a'</span>,a,<span class="hljs-string">'b'</span>,b]),Result,err);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这个客户端-服务端在这里使用JSON，服务端使用<code>Ctxt.Results()</code>方法编码，但您可以对服务端通过重载<code>Ctxt.Returns()</code>的内容类型来提供任何类型的数据，二进制、HTML等等....。</p>
<h2 id="toc_3">14.3. 服务端的直接参数解析<a class="vnote-anchor" href="#toc_3" data-anchor-icon="#"></a></h2>
<p>  我们使用如上所述的<code>Ctxt[]</code>和<code>Ctxt.Input*[]</code>属性来检索输入参数。 这非常容易使用且功能强大，但提供的Ctxt完全可以访问输入和输出上下文。</p>
<p>  以下是我们如何实现最快的参数解析，请参阅示例Project06Server.dpr：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRestServerTest</span>.<span class="hljs-title">Sum</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
<span class="hljs-keyword">var</span> a,b: double;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> UrlDecodeNeedParameters(Ctxt.Parameters,<span class="hljs-string">'A,B'</span>) <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
    <span class="hljs-keyword">while</span> Ctxt.Parameters&lt;&gt;<span class="hljs-keyword">nil</span> <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
      UrlDecodeDouble(Ctxt.Parameters,<span class="hljs-string">'A='</span>,a);
      UrlDecodeDouble(Ctxt.Parameters,<span class="hljs-string">'B='</span>,b,@Ctxt.Parameters);
    <span class="hljs-keyword">end</span>;
    Ctxt.Results([a+b]);
  <span class="hljs-keyword">end</span> <span class="hljs-keyword">else</span>
    Ctxt.Error(<span class="hljs-string">'Missing Parameter'</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  此代码中唯一不明显的部分是参数解析，即如何从传入的<code>Ctxt.Parameters</code>文本缓冲区中检索值，然后将其转换为本机局部变量。</p>
<p>  在服务器端，典型的实现步骤是：</p>
<ul>
<li>使用<code>UrlDecodeNeedParameters</code>函数检查<code>Ctxt.Parameters</code>中调用者是否提供了所有预期参数;</li>
<li>调用<code>UrlDecodeInteger / UrlDecodeInt64 / UrlDecodeDouble / UrlDecodeExtended / UrlDecodeValue / UrlDecodeObject</code>函数（均在SynCommons.pas中定义）从标准JSON内容中检索每个单独的参数;</li>
<li>实现服务（这里是<code>a+b</code>表达式）;</li>
<li>然后调用<code>Ctxt.Results()</code>方法返回结果或在出现错误时调用<code>Ctxt.Error()</code>。</li>
</ul>
<p>  强大的<code>UrlDecodeObject</code>函数（在<code>mORMot.pas</code>中定义）可用于从JSON文本表示（TPersistent、TSQLRecord、TStringList ...）中反序列化大多数类实例。</p>
<p>  使用<code>Ctxt.Results()</code>将指定的值编码为拥有<code>"Result"</code>成员的JSON对象，默认为mime-类型为<code>JSON_CONTENT_TYPE</code>：</p>
<pre><code class="lang-json hljs"> {<span class="hljs-attr">"Result"</span>:<span class="hljs-string">"OneValue"</span>}
</code></pre>
<p>  或者包含数组的JSON对象：</p>
<pre><code class="lang-json hljs"> {<span class="hljs-attr">"Result"</span>:[<span class="hljs-string">"One"</span>,<span class="hljs-string">"two"</span>]}
</code></pre>
<h2 id="toc_4">14.4. 返回非JSON内容<a class="vnote-anchor" href="#toc_4" data-anchor-icon="#"></a></h2>
<p>  使用<code>Ctxt.Returns()</code>将允许该方法返回以任意格式内容，如作为JSON对象（通过重载<code>Ctxt.Returns([])</code>方法提供期望字段名/值对）或任何内容，因为返回的MIME类型可以定义为<code>Ctxt.Returns()</code>的参数 - 它可以用于指定另一个mime类型而不是默认常量<code>JSON_CONTENT_TYPE</code>，如<code>'application/json; charset=UTF-8'</code>，并返回纯文本、HTML或二进制文件。</p>
<p>  例如，您可以直接返回纯文本值：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRestServer</span>.<span class="hljs-title">Timestamp</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
<span class="hljs-keyword">begin</span>
  Ctxt.Returns(Int64ToUtf8(ServerTimestamp),HTTP_SUCCESS,TEXT_CONTENT_TYPE_HEADER);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  或者您可以返回一些二进制文件，根据二进制内容中生成相应的MIME类型：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRestServer</span>.<span class="hljs-title">GetFile</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
<span class="hljs-keyword">var</span> fileName: TFileName;
    content: RawByteString;
    contentType: RawUTF8;
<span class="hljs-keyword">begin</span>
  fileName :=  <span class="hljs-string">'c:\data\'</span>+ExtractFileName(Ctxt[<span class="hljs-string">'filename'</span>]); <span class="hljs-comment">// or Ctxt.Input['filename']</span>
  content := StringFromFile(fileName);
  <span class="hljs-keyword">if</span> content=<span class="hljs-string">''</span> <span class="hljs-keyword">then</span>
    Ctxt.Error(<span class="hljs-string">''</span>,HTTP_NOTFOUND) <span class="hljs-keyword">else</span>
    Ctxt.Returns(content,HTTP_SUCCESS,HEADER_CONTENT_TYPE+
         GetMimeContentType(pointer(content),Length(content),fileName));
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  相应的客户端方法可以这样定义：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TMyClient</span>.<span class="hljs-title">GetFile</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aFileName: RawUTF8)</span>:</span> RawByteString;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> CallBackGet(<span class="hljs-string">'GetFile'</span>,[<span class="hljs-string">'filename'</span>,aFileName],RawUTF8(result))&lt;&gt;HTTP_SUCCESS <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.CreateFmt(<span class="hljs-string">'Impossible to get file: %s'</span>,[result]);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  请注意，<code>Ctxt.ReturnFile()</code>方法比<code>TSQLRestServer.GetFile()</code>方法实现的手动文件检索更受欢迎。此处仅用于演示目的。</p>
<p>  如果您使用HTTP作为通信协议，您可以使用这些服务，在Delphi代码中快速实现服务端，并在客户端使用任何AJAX应用程序。</p>
<p>  发送非JSON内容时使用<code>GetMimeContentType()</code>（例如图片，pdf文件，二进制...）将被任何标准互联网浏览器按预期解释：它可用于在页面内很好的提供一些旧HTML内容，而不必使用JavaScript提供的服务。</p>
<h2 id="toc_5">14.5. 服务端高级过程<a class="vnote-anchor" href="#toc_5" data-anchor-icon="#"></a></h2>
<p>  在服务器端，方法定义只有一个<code>Ctxt</code>参数，该参数在调用时拥有多个成员，发布了所有服务调用特性和上下文，包括RESTful URI路由、会话处理或底层HTTP头(如果有的话)。</p>
<p>  首先，可以从RESTful URI解码<code>Ctxt</code>并获取需要的<code>TSQLRecord</code> ID和<code>TSQLRecord</code>类。这意味着服务可以与我们的ORM框架的任何表/类相关联，因此您将能够轻松地在URI上创建任何RESTful兼容请求，如<code>ModelRoot/TableName/TableID/MethodName</code>。记录ID从其RESTful方案解码为<code>Ctxt.TableID</code>，表对应<code>Ctxt.Table</code>或<code>Ctxt.TableIndex</code>（如果您需要在关联的服务器模型中使用其索引）。</p>
<p>  例如，这里我们根据<code>TableName/TableID</code>返回为十六进制BLOB字段内容：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRestServerTest</span>.<span class="hljs-title">DataAsHex</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
<span class="hljs-keyword">var</span> aData: TSQLRawBlob;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> (self=<span class="hljs-keyword">nil</span>) <span class="hljs-keyword">or</span> (Ctxt.Table&lt;&gt;TSQLRecordPeople) <span class="hljs-keyword">or</span> (Ctxt.TableID&lt;=<span class="hljs-number">0</span>) <span class="hljs-keyword">then</span>
    Ctxt.Error(<span class="hljs-string">'Need a valid record and its ID'</span>) <span class="hljs-keyword">else</span>
  <span class="hljs-keyword">if</span> RetrieveBlob(TSQLRecordPeople,Ctxt.TableID,<span class="hljs-string">'Data'</span>,aData) <span class="hljs-keyword">then</span>
    Ctxt.Results([SynCommons.BinToHex(aData)]) <span class="hljs-keyword">else</span>
    Ctxt.Error(<span class="hljs-string">'Impossible to retrieve the Data BLOB field'</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>相应的客户端方法可以是：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TSQLRecordPeople</span>.<span class="hljs-title">DataAsHex</span><span class="hljs-params">(aClient: TSQLRestClientURI)</span>:</span> RawUTF8;
<span class="hljs-keyword">begin</span>
  Result := aClient.CallBackGetResult(<span class="hljs-string">'DataAsHex'</span>,[],RecordClass,fID);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  如果使用身份验证（请参阅下文），则<code>Session / SessionUser / SessionGroup</code>字段中可以使用当前会话、用户和组ID。如果身份验证不可用，则这些字段无意义：实际上，如果尚未启动任何会话，则Ctxt.Context.Session将包含0（<code>CONST_AUTHENTICATION_SESSION_NOT_STARTED</code>），如果身份验证模式未激活，则包含1（<code>CONST_AUTHENTICATION_NOT_USED</code>）。服务器端实现可以使用<code>TSQLRestServer.SessionGetUser</code>方法来检索相应的用户详细信息（请注意，使用此方法时，返回的<code>TSQLAuthUser</code>实例是本地线程安全副本，完成后应释放该副本）。</p>
<p>  在<code>Ctxt.Call^</code>成员中，您可以访问底层通信内容，即所有传入和传出值，包括报文头和邮件正文。根据所使用的传输协议，您可以检索HTTP报文头信息等信息。例如，以下是您可以访问底层的客户端远程IP地址和应用程序User-Agent的方法：</p>
<pre><code class="lang-pascal hljs"> aRemoteIP := FindIniNameValue(pointer(Ctxt.Call.InHead),<span class="hljs-string">'REMOTEIP: '</span>);
 aUserAgent := FindIniNameValue(pointer(Ctxt.Call.InHead),<span class="hljs-string">'USER-AGENT: '</span>);
</code></pre>
<p>  当然，对于这些字段，更优选使用<code>Ctxt.RemoteIP</code>或<code>Ctxt.UserAgent</code>属性，这些属性使用有效的缓存。</p>
<h2 id="toc_6">14.6. 浏览器加速未修改的请求<a class="vnote-anchor" href="#toc_6" data-anchor-icon="#"></a></h2>
<p>  当通过慢速网络（例如通过Internet）使用时，您可以设置<code>Ctxt.Returns()</code>和<code>Ctxt.Results()</code>方法的可选<code>Handle304NotModified</code>参数，以便仅返回修改过的响应正文。</p>
<p>  在实践中，结果内容将被散列（使用crc32c算法和快速SSE4.2硬件指令，如果可用）并且在没有修改的情况下将向浏览器返回“304 Not Modified”状态，而没有实际结果内容。因此，响应将更快地发送和接收，并将节省大量带宽，尤其是在周期性服务池（如用于客户端屏幕刷新）的情况下。</p>
<p>  请注意，在crc32c算法与哈希冲突的情况下（我们从未见过它，但存在这样的数学可能性），可能会返回误报“未修改”状态；因此，默认情况下不设置此选项，并且仅当您的客户端不处理任何敏感的记帐过程时才应启用此选项。</p>
<p>  请注意，您应通过<code>TSQLRestServer.ServiceMethodByPassAuthentication()</code>调用对使用<code>Handle304NotModified</code>参数的方法禁用身份验证。事实上，我们的RESTful身份验证 ，使用per-URI签名，这种签名经常发生变化（以避免中间人攻击）。因此，如果使用身份验证，任何浏览器端缓存优势都将无效：浏览器内部缓存将不会增长，因为之前的URI已被弃用，并且在大多数情况下它将是缓存缺失。但是当提供一些静态内容（例如HTML内容、固定的JSON值甚至UI二进制文件）时，这种浏览器端缓存可能非常有用。</p>
<p>  即使使用外部内容分发网络（CDN）服务，此无状态REST模型也将启用多个级别的缓存。请参阅潜在托管架构，这些架构可能会让您的mORMot服务器扩展到数千个并发用户，并以最佳的响应速度在全球范围内提供服务。</p>
<h2 id="toc_7">14.7. 返回文件内容<a class="vnote-anchor" href="#toc_7" data-anchor-icon="#"></a></h2>
<p>  框架的HTTP服务器能够处理将文件作为对基于方法的服务的响应返回。</p>
<p>  高性能的<code>http.sys</code>服务器甚至能够从内核模式异步地提供文件内容，具有出色的性能。</p>
<p>  您可以使用<code>Ctxt.ReturnFile()</code>方法直接返回文件。</p>
<p>  此方法还能够从文件扩展名中猜出MIME类型，并且如果<code>Handle304NotModified</code>参数为true，则使用文件时间戳处理<code>HTTP_NOTMODIFIED = 304</code>进程。</p>
<p>  另一种可能性是使用<code>Ctxt.ReturnFileFromFolder()</code>方法，该方法能够从本地文件夹有效地返回由其URI指定的任何文件，可以非常方便从mORMot HTTP服务器返回一些静态Web内容。</p>
<h2 id="toc_8">14.8. JSON Web令牌（JWT）<a class="vnote-anchor" href="#toc_8" data-anchor-icon="#"></a></h2>
<p>  JSON Web Token（<code>JWT</code>）是一个开放标准（RFC 7519），它定义了一种紧凑且独立的方式，可以在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。 JWT可以使用秘密（使用HMAC算法）或使用RSA或ECDSA的公钥/私钥对进行签名。</p>
<p>  它们可用于：</p>
<ul>
<li>身份验证：包括任何HTTP请求的JWT允许跨不同域的单点登录用户验证；</li>
<li>安全信息交换：可以将少量数据存储在JWT载体中，并进行数字签名以确保其出处和完整性。</li>
</ul>
<p>  有关JSON Web令牌的介绍，请参见http://jwt.io。</p>
<p>  我们的框架实现了JWT：</p>
<ul>
<li><code>"HS256"</code>（HMAC-SHA256）和<code>"ES256"</code>（256位ECDSA）算法， 加上<code>"none"</code>弱算法，谨慎使用；</li>
<li>计算并验证所有JWT声明：日期，audiences，JWT ID；</li>
<li>线程安全和高性能（在x64下对于HS256验证为2us），可选的内存缓存（如对于较慢的ES256）;</li>
<li>独立和跨平台代码：无外部DLL，适用于Delphi或FPC；</li>
<li>增强安全性 - 它的设计根据https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries</li>
<li>与框架完整集成。</li>
</ul>
<p>  它围绕一组类构建，每个算法一个，遵循最小惊奇原理，并增强安全性：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 458 198" style="max-width:458px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-1 .node&gt;rect { ; }
#mermaid-diagram-1 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-1 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-1 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-1 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M69.5,64L69.5,89L163.5,118.25827814569536" marker-end="url(#arrowhead9)" style="fill:none"></path><defs><marker id="arrowhead9" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M220.5,64L220.5,89L220.5,114" marker-end="url(#arrowhead10)" style="fill:none"></path><defs><marker id="arrowhead10" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M370,64L370,89L277.5,118.08026755852843" marker-end="url(#arrowhead11)" style="fill:none"></path><defs><marker id="arrowhead11" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A" transform="translate(69.5,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-49.5" y="-22" width="99" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.5,-12)"><foreignObject width="79" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TJWTES256</div></foreignObject></g></g></g><g class="node cyan" id="D" transform="translate(220.5,136)" style="opacity: 1;"><rect rx="0" ry="0" x="-57" y="-22" width="114" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47,-12)"><foreignObject width="94" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TJWTAbstract</div></foreignObject></g></g></g><g class="node cyan" id="B" transform="translate(220.5,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-51.5" y="-22" width="103" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-41.5,-12)"><foreignObject width="83" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TJWTHS256</div></foreignObject></g></g></g><g class="node cyan" id="C" transform="translate(370,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-48" y="-22" width="96" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38,-12)"><foreignObject width="76" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TJWTNone</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  在<code>SynCrypto.pas</code>和<code>SynEcc.pas</code>中，您会发现：</p>
<ul>
<li>TJWTAbstract是实现JSON Web令牌的抽象父类；</li>
<li>TJWTNone实现<code>"none"</code>算法;</li>
<li>TJWTHS256实现<code>"HS256"</code>算法;</li>
<li>TJWTES256实现了<code>"ES256"</code>算法。</li>
</ul>
<p>  要使用JWT，您可以编写例如：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> j: TJWTAbstract;
    jwt: TJWTContent;
...
j := TJWTHS256.Create(<span class="hljs-string">'secret'</span>,<span class="hljs-number">0</span>,[jrcSubject],[]);
<span class="hljs-keyword">try</span>
  j.Verify(<span class="hljs-string">'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm'</span>+
    <span class="hljs-string">'FtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeF'</span>+
    <span class="hljs-string">'ONFh7HgQ'</span>,jwt); <span class="hljs-comment">// reference from jwt.io</span>
  check(jwt.result=jwtValid);
  check(jwt.reg[jrcSubject]=<span class="hljs-string">'1234567890'</span>);
  check(jwt.data.U[<span class="hljs-string">'name'</span>]=<span class="hljs-string">'John Doe'</span>);
  check(jwt.data.B[<span class="hljs-string">'admin'</span>]);
<span class="hljs-keyword">finally</span>
  j.Free;
<span class="hljs-keyword">end</span>;
</code></pre>
<p><code>'eyJhbGciOiJIUzI1NiIsIn...'</code>令牌实际上包含以下内容，用base-64解码：</p>
<ul>
<li>header：<code>{"alg":"HS256","typ":"JWT"}</code></li>
<li>负荷：<code>{"sub":"1234567890","name":"John Doe","admin":true}</code></li>
<li>签名：<code>HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), "secret")</code></li>
</ul>
<p>  <code>TJWTAbstract</code>类根据给定JWT集的安全参数完成逻辑实现，然后您可以使用<code>TJWTAbstract.Verify</code>来解码并检查JWT的有效负载和签名到<code>TJWTContent</code>局部变量中。 如<code>TJWTContent.result</code>包含解码状态，<code>TJWTContent.reg[]</code>包含解码声明，<code>TJWTContent.data</code>是<code>TDocVariant</code>自定义变体类型，可以访问任何存储的私人信息。</p>
<p>  它在生成令牌时内置支持JWT声明，因此您可以编写：</p>
<pre><code class="lang-pascal hljs">j := TJWTHS256.Create(<span class="hljs-string">'sec'</span>,<span class="hljs-number">10</span>,[jrcIssuer,jrcExpirationTime,jrcIssuedAt,jrcJWTID],[],<span class="hljs-number">60</span>);
token := j.Compute([<span class="hljs-string">'http://example.com/is_root'</span>,true],<span class="hljs-string">'joe'</span>);
</code></pre>
<p>  现在，<code>token</code>变量包含如有效载荷签名：</p>
<pre><code class="lang-json hljs">{<span class="hljs-attr">"http://example.com/is_root"</span>:<span class="hljs-literal">true</span>,<span class="hljs-attr">"iss"</span>:<span class="hljs-string">"joe"</span>,<span class="hljs-attr">"iat"</span>:<span class="hljs-number">1482177879</span>,<span class="hljs-attr">"exp"</span>:<span class="hljs-number">1482181479</span>,<span class="hljs-attr">"jti"</span>:<span class="hljs-string">"1496DCE0676925DD33BB5A81"</span>}
</code></pre>
<p>  发行者被编码为<code>"iss":</code>字段，<code>"iat"</code>和<code>"exp"</code>字段包含发布和到期时间戳，并且<code>"jti"</code>已经用<code>TSynUniqueIdentifier</code>填充为JWT ID。 由于我们使用了<code>TJWTHS256</code>类，因此附加了头部和有效负载的<code>HMAC-SHA256</code>数字签名 - 使用10轮PBKDF2_HMAC_SHA256派生从<code>'sec'</code>密码短语安全地推导出秘语（实际上，您可以使用更高的数字，如20,000）。</p>
<p>  然后，您可以解码此令牌，并使用单个方法访问其有效内容：</p>
<pre><code class="lang-pascal hljs">j.Verify(token,jwt);
assert(jwt.result=jwtValid);
assert(jwt.reg[jrcIssuer]=<span class="hljs-string">'joe'</span>);
</code></pre>
<p>  使用<code>TSQLRestServerURIContext.AuthenticationCheck</code>方法可以轻松地与基于方法的服务集成：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-title">TMyDaemon</span> = <span class="hljs-keyword">class</span>(...
<span class="hljs-keyword">protected</span>
  fJWT: TJWTAbstract;
  ....
<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TMyDaemon</span>.<span class="hljs-title">Files</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> Ctxt.AuthenticationCheck(fJWT)=jwtValid <span class="hljs-keyword">then</span>
    Ctxt.ReturnFileFromFolder(<span class="hljs-string">'c:\datafolder'</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  上述方法将定义一个基于方法的服务，仅当传入请求的HTTP头中提供了有效的JWT时，该服务才返回本地文件夹的内容。如果<code>AuthenticationCheck</code>未能验证关联Ctxt中提供的令牌，则将按预期将<code>401 http_authorized</code>返回给客户机。</p>
<h2 id="toc_9">14.9. 错误处理<a class="vnote-anchor" href="#toc_9" data-anchor-icon="#"></a></h2>
<p>  使用<code>Ctxt.Input*[]</code>属性时，任何缺少的参数都将引发<code>EParsingException</code>。因此，它将被服务器进程拦截（与任何其他异常一样），并返回到客户端，并显示包含Exception类名及其关联消息的错误消息。</p>
<p>  但是，如果需要，您可以完全的访问错误流程。实际上，调用<code>Ctxt.Results(), Ctxt.Returns(), Ctxt.Success()</code>或<code>Ctxt.Error()</code>将指定HTTP状态代码（例如，<code>Results()</code>和<code>Success()</code>方法默认为200 / "OK"，<code>Error()</code>为400 / "Bad Request"）作为整数值。例如，以下是不返回任何内容的服务如何处理这些状态/错误代码：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TSQLRestServer</span>.<span class="hljs-title">Batch</span><span class="hljs-params">(Ctxt: TSQLRestServerURIContext)</span>;</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> (Ctxt.Method=mPUT) <span class="hljs-keyword">and</span> RunBatch(<span class="hljs-keyword">nil</span>,<span class="hljs-keyword">nil</span>,Ctxt) <span class="hljs-keyword">then</span>
    Ctxt.Success <span class="hljs-keyword">else</span>
    Ctxt.Error;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  如果服务端出现错误，您可以调用<code>Ctxt.Error()</code>方法（只有两个有效的状态代码是200和201）。</p>
<p>  <code>Ctxt.Error()</code>方法有一个可选参数，用于指定自定义错误消息，在返回无效状态码时，将返回给客户端。 如果未指定自定义文本，则框架将返回相应的通用HTTP状态文本（如对于状态码<code>HTTP_BADREQUEST = 400</code>默认为<code>"Bad Request"</code>）。</p>
<p>  在这种情况下，客户端将接收相应的JSON序列化错误对象，例如， 对于<code>Ctxt.Error('Missing Parameter',HTTP_NOTFOUND)</code>：</p>
<pre><code class="lang-json hljs">{
 <span class="hljs-attr">"ErrorCode"</span>:<span class="hljs-number">404</span>,
 <span class="hljs-attr">"ErrorText"</span>:<span class="hljs-string">"Missing Parameter"</span>
}
</code></pre>
<p>  如果从AJAX客户端或浏览器调用，则此内容应易于理解。</p>
<p>  请注意，框架核心将在方法执行期间捕获任何异常，并返回<code>"Internal Server Error" / HTTP_SERVERERROR = 500</code>错误代码以及相关的文本异常详细信息。</p>
<h2 id="toc_10">14.10. 该实现的优势与局限性<a class="vnote-anchor" href="#toc_10" data-anchor-icon="#"></a></h2>
<p>  基于方法的服务允许通过我们框架的所有常用协议快速、直接地访问所有mORMot Client-Server RESTful功能：HTTP/1.1，命名管道、Windows消息、直接内存/进程内访问。</p>
<p>  mORMot实现的基于方法的服务提供了对框架核心最底层的完全访问，因此它具有以下优点：</p>
<ul>
<li>它可以调整为适合任何目的（例如检索或返回一些HTML或二进制数据，或动态修改HTTP头）;</li>
<li>它集成到RESTful URI模型中，因此它可以与我们的ORM框架的任何表/类（如上面的DataAsHex服务）相关，或者它可以处理任何远程查询（例如任何AJAX或SOAP请求）;</li>
<li>它具有非常低的性能开销，因此可用于减少某些常见任务的服务器工作负载。</li>
</ul>
<p>  请注意，由于此实现模式，mORMot服务实现速度非常快，并且对Apache报告的“哈希冲突攻击”安全问题不敏感 - 详情请参阅http://blog.synopse.info/post/2011/12/30/Hash-collision-attack。</p>
<p>  这种实现，许多过程（例如参数编组）将在客户端和服务器端代码上手动完成。此外，使用“逐个方法”构建和维护庞大的SOA系统可能很困难，因为它发布了一个大的“扁平”服务集，这是接口进入场景。</p>

    </div>
</div>
</div>

<div id="container-floating" style="display:none;" class="d-none d-md-block d-xl-block">
    <div id="floating-button" onclick="toggleMore()">
        <p id="floating-more" class="more">&gt;</p>
    </div>
</div>

<!--
<div class="footer" id="vnote-footer">
    <p>Generated by <em><a href="https://tamlok.github.io/vnote/">VNote</a></em>.</p>
</div>
-->
</body>
</html>
